Comprehensions, Maps, Lambdas, Generators and Decorators
The following are frequently used in Python and a good understanding of each is required.
Comprehensions
Comprehensions in Python provide a concise method of creating a new data structure from an existing one whilst filtering and or modifying the data.
List comprehensions
A list as we know is a sequence of data values akin to arrays in other languages. We know how to iterate over lists using 'For' and 'While' loops, let's look at how we use a comprehension to iterate over a list.
Prime Numbers Take a list of numbers from 2 to 100 and create a list of all the prime numbers.
For those who have forgotten all about prime numbers here is a quick recap.
A prime number is a number, excluding 1, that can be divided only by itself or 1. For example, 2 can be divided by itself or 1 and is therefore a prime number. 6 on the other hand can be divided by itself, 1, 2 and 3, therefore it is not a prime number.
Before we look at the list comprehension for prime numbers, let's look at a standard method using 'for loops'.
for x in range(2, 101):
for y in range(2, x):
if x % y == 0:
break
else:
print(x, end=" ")
Here we use two for loops, one nested inside the other. The second with a simple conditional expression using the %
modulus operator. Each number between 2 and 101 is
divided by all numbers up until itself until the modulus is 0. If it is never 0 then the number is a prime number.
One other interesting thing about the above for loop is the use of the else
under the for
and not the if
. Normally we assoicate an else
with and if
statement, but
in Python you can use it to execute code after the loop has expired. In this case it will obly get executed if the break
statement in the if
statement is not applied.
The break
statement will exit the loop completely.
Run that and note the prime numbers.
Now let's translate that to a list comprehension.
primes = [x for x in range(2, 101) if all(x % y != 0 for y in range(2, x))]
print(primes)
The above comprehension is almost the same except that it is contained within []
to indicate that the result should be a list called primes
. Each x
that is a prime
number will be placed in the list primes
. The ordering of the loop constructs is also different with the use of if
with an all
operator to indicate that we want to test
each number in the range 2 to x
represented as y
.
Dictionary comprehension
Pretty much the same as list comprehensions but dealing with dictionary key-value pairs instead of indexed items. Obviously the result is a dictionary.
new_dict = {k:v for k,v, in {1: 'a', 2: 'b', 3: 'c', 4:'d'}.items() if v < 'c'}
print(new_dict)
Tuple comprehensions??
There is no tuple comprehension in Python, but you can get the results of a list or dictionary comprehension into a tuple by wrapping a list comprehension in a tuple
function.
If you did the same for a dictionary comprehension you would just get the relevant keys. Try it out.
primes = tuple([x for x in range(2, 101) if all(x % y != 0 for y in range(2, x))])
print(primes) # Prints a tuple containing the prime numbers
Maps
Python maps are generally used to iterate over data structures applying functions or expressions to the items in the data structure. You can think of it as mapping expressions over data.
Let's look at an example
def square(x):
return x ** 2
a_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b_list = list(map(square, a_list))
print(b_list)
What's happening here is the map
function has two parameters square
, the name of the function to call and a_list
the list of numbers.
For each number in the list a_list
, the map function calls the function square
and passes the number as a parameter. The square function
does its work
and returns the result.
Note calling a function without a return value is not going to work as the result of the mapping will be lost.
The results of the mapping function above are placed in a list, as can be seen we wrap the map()
function call in a list()
function which ends up in the variable b_list
The following example will convert strings to uppercase.
def upper(x):
return x.upper()
a_list = ["richard", "tayfun", "pablo"]
b_list = list(map(upper, a_list))
print(b_list)
Lambdas
Python Lambda functions are often called anonymous functions. Unlike normal functions, Lambdas are shorthand, mostly one off usage functions in the sense that they are
not required as a reusable function that is callable from multiple locations in the code. A typical use is a one of calculation for a map
function.
Let's take our square root example above and make it use a lambda function instead of a normal function.
a_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b_list = (list(map(lambda x: x ** 2, a_list)))
print(b_list)
Filtering a list
a_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b_list = (list(filter(lambda x: x > 5, a_list)))
print(b_list)
Celsius to Fahrenheit conversion))
a_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b_list = (list(map(lambda x: (x * 1.8000) + 32, a_list)))
print(b_list)
Now we have a one liner map
function with a lambda function and a list as the two components.
You can also return a lambda function from a normal function and call it with a value.
def multiply (n):
return lambda x: x*n
by_five = multiply(5)
by_ten = multiply(10)
by_fifteen = multiply(15)
print(by_five(5))
print(by_ten(5))
print(by_fifteen(5))
Generators
Python generators are instances of iterator functions that use a yield
statement to return the code event control back to the calling code. Hence, any function that has a
yield
statement in it can be considered a generator. To yield means to give back control, in this case to the code that called the generator.
As with all iterators you can invoke the next
function to continue with its execution.
Generators have numerous usages, not least walking through large sets of data asynchronously. Imagine you want all the records in a database that have a certain key in them, but you want to process those records as they arrive rather than collect them all first. Using a generator can facilitate that.
Here we shall stick with a few introductory examples to get you familiar with the concepts.
Recalling the prime number loops and comprehension earlier, i.e. the following example
for x in range(2, 101):
for y in range(2, x):
if x % y == 0:
break
else:
print(x, end=" ")
In the for loop above we continue the loop until all prime numbers from 2 to 100 have been found. Let's turn this into a generator function
def prime_generator():
for x in range(2, 101):
for y in range(2, x):
if x % y == 0:
break
else:
yield x
for n in prime_generator():
print(n, end=" ")
print("times 2 = ", n*2)
The yield
in the prime_generator
function yields the variable x
and returns to the calling loop which does some work, in this case a couple of print statements.
We can use the generator with the next
function thus,
def prime_generator():
for x in range(2, 101):
for y in range(2, x):
if x % y == 0:
break
else:
yield x
primes = prime_generator()
print(next(primes))
print(next(primes))
print(next(primes))
One last example takes us back to the fact that in Python Tuples do not have comprehensions. However, it is possible to generate a generator object from what looks like a tuple comprehension.
# Creates a generator
primes = (x for x in range(2, 101) if all(x % y != 0 for y in range(2, x)))
# Iterate over the generator using the next function.
print(next(primes))
print(next(primes))
print(next(primes))
print(next(primes))
print(next(primes))
print(next(primes))
Decorators
Python decorators are basically pointers to other functions that you wish to run before proceeding with some other function call.
A Python decorator name is preceded by an @
symbol as in @my_decorator
. Decorator functions generally have inner functions that do the work of the decorator.
Decorators are commonly used to apply criteria for using functions, such as login control, access permissions, filters, conversions or expressions on data etc. etc.
We'll take a look at a couple of uses below, the first of which will use our prime number generator as an example.
def generate_random(func):
import random
def get_randoms():
"""
Inner decorator function that does the heavy lifting
"""
x = random.randrange(10)
y = random.randrange(10, 102)
return func(x, y)
return get_randoms
@generate_random
def prime_numbers(r1, r2):
print(f"generating primes from {r1} to {r2}")
return (x for x in range(r1, r2) if all(x % y != 0 for y in range(2, x)))
primes = prime_numbers()
for i in primes:
print(i)
We have a function prime_numbers
that is decorated with a decorator called generate_random
, which is in itself a function that generates two random numbers
and passes those back to an instance of the prime_numbers
function.
The decorator function has two parts, the main decorator function generate_random
, which has a parameter func
, a reference to the function that it is decorating,
i.e. prime_numbers
, and an inner function, get_randoms
. This is the core part of the decorator that generates the two random numbers.
The inner function is called using return get_randoms
from the main decorator function, and returns an instance of the func
function reference with the two random numbers,
which the main decorator function returns as a consequence of the return get_randoms
statement.
Notice the lack of
()
brackets in the call toget_randoms
.
Our next example represents a cut down version of a more fruitful use of decorators, that of access role control.
def permissions(required_access_role):
def is_accessible(func):
def wrapper(user_access_role):
if required_access_role == user_access_role:
return func(user_access_role)
else:
return False
return wrapper
return is_accessible
@permissions("admin")
def do_admin_work(access_role):
print("Doing very important admin work")
do_admin_work("basic")
This is a tad more complicated than the first decorator, instead of a single inner function it has a second inner function inside the first inner function.
The reason for this is we want to use the parameters from the decorated function do_admin_work
, explicitly the access_role
parameter. Using this second function allows
us that access. As can be seen we have a parameter access_role
for the wrapper
function.
This decorator also has its own parameter required_access_role
which is set to admin
. What this is telling us is that to access the do_admin_work
function you need to have
an access_role that is admin.
The decorator compares the required_access_role
sent as a parameter in the actual decorator call @permissions("admin")
and the user_access_role
that is a parameter of the
do_admin_work
function. If they match it return the function which runs. If they don't match it returns false and the function does not get called. Normally you would handle
the mismatch with some form of exception handling, but we haven't covered this subject yet.
That's it for this section, you can move on to the next part of this tutorial.